﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Aga.Controls.Tree;
using System.IO;
using ICSharpCode.TextEditor.Document;
using System.Diagnostics;
using ICSharpCode.TextEditor;
using UnrealDebugger.Panels;
using System.Reflection;
using WeifenLuo.WinFormsUI.Docking;

namespace UnrealDebugger.Panels
{
    public partial class FindResults : DockContent
    {
        private static int _searchCount = 0;

        private string _text = "";
        private bool _matchCase = false;
        private bool _matchWholeWordOnly = false;
        private SearchTreeModel _model = null;
        private List<ToolStripButton> _highlightButtons = new List<ToolStripButton>();

        public delegate void HighlightColorChangedDelegate(Color color);
        public event HighlightColorChangedDelegate OnHighlightColorChanged;

        #region Search Tree Model

        public class DocumentNode
        {
            private Document _document = null;

            public DocumentNode()
            {
            }

            public DocumentNode(Document document)
            {
                this._document = document;
            }

            public bool IsUnrealFile()
            {
                if (Document != null)
                {
                    string extension = Path.GetExtension(Document.FileName);
                    return (string.Compare(extension, ".uc", true) == 0) || (string.Compare(extension, ".uci", true) == 0);
                }

                return false;
            }

            public Document Document { get { return _document; } }
            public Image Icon { get { return IsUnrealFile() ? _script : _file; } }

            public HighlightGroup HighlightGroup { get; set; }

            public string Text 
            { 
                get 
                { 
                    return this._document != null ? Path.GetFileName(this._document.FileName) : "Error"; 
                } 
            }

            protected static Image _file = null;
            protected static Image _script = null;

            static DocumentNode()
            {
                Stream file = null;
                Assembly assembly = typeof(DocumentNode).Assembly;

                file = assembly.GetManifestResourceStream("UnrealDebugger.Resources.File.png");
                _file = Image.FromStream(file);

                file = assembly.GetManifestResourceStream("UnrealDebugger.Resources.Script.png");
                _script = Image.FromStream(file);
            }
        }

        public class LineNode
        {
            private Document _document = null;
            private int _offset = 0;

            public LineNode()
            {
            }

            public LineNode(Document document, int offset)
            {
                this._document = document;
                this._offset = offset;
            }

            public void GotoLine()
            {
                if (this._document != null)
                {
                    this._document.Show();
                    this._document.GotoLine(this.Line, 0);
                }
            }

            public string Text 
            { 
                get 
                {
                    IDocument document = this._document.TextEditor.Document;

                    if (document != null)
                    {
                        return TextUtilities.GetLineAsString(document, Line - 1).TrimStart(new char[] { ' ', '\t'});
                    }
                    else
                    {
                        return "Error";
                    }
                } 
            }

            public int Line
            {
                get
                {
                    IDocument document = this._document.TextEditor.Document;

                    if (document != null)
                    {
                        TextLocation location = document.OffsetToPosition(this._offset);
                        return location.Line + 1;
                    }
                    else
                    {
                        return 0;
                    }
                }
            }
        }

        public class DocumentNodeComparer : IComparer<DocumentNode>
        {
            public int Compare(DocumentNode a, DocumentNode b)
            {
                return string.Compare(a.Text, b.Text, true);
            }
        }

        public class SearchTreeModel : TreeModelBase
        {
            private string _text = "";
            private bool _matchCase = false;
            private bool _matchWholeWordOnly = false;
            private Color _color = Color.LemonChiffon;
            private List<DocumentNode> _nodes = new List<DocumentNode>();

            public SearchTreeModel(string text, bool matchCase, bool matchWholeWordOnly, Color color)
            {
                this._text = text;
                this._matchCase = matchCase;
                this._matchWholeWordOnly = matchWholeWordOnly;
                this._color = color;
            }

            public override System.Collections.IEnumerable GetChildren(TreePath treePath)
            {
                if (treePath.FullPath.Length <= 0)
                {
                    List<Document> documents = UnrealDebuggerIDE.Instance.GetOpenDocuments();
                    documents.Sort(delegate(Document a, Document b) { return string.Compare(a.FileName, b.FileName, true); });

                    foreach (Document document in documents)
                    {
                        DocumentNode node = new DocumentNode(document);
                        node.HighlightGroup = new HighlightGroup(node.Document.TextEditor);

                        this._nodes.Add(node);
                        yield return node;
                    }
                }
                else
                {
                    DocumentNode node = treePath.LastNode as DocumentNode;

                    if (node != null)
                    {
                        IDocument document = node.Document.TextEditor.Document;
                        TextEditorSearcher searcher = new TextEditorSearcher();
                        searcher.Document = document;

                        searcher.LookFor = _text;
                        searcher.MatchCase = _matchCase;
                        searcher.MatchWholeWordOnly = _matchWholeWordOnly;

                        bool looped = false;
                        int offset = 0, count = 0;
                        for (; ; )
                        {
                            TextRange range = searcher.FindNext(offset, false, out looped);
                            if (range == null || looped)
                                break;
                            offset = range.Offset + range.Length;
                            count++;

                            var m = new TextMarker(range.Offset, range.Length,
                                    TextMarkerType.SolidBlock, _color, Color.Black);
                            node.HighlightGroup.AddMarker(m);

                            yield return new LineNode(node.Document, range.Offset);
                        }
                    }
                }
            }

            public override bool IsLeaf(TreePath treePath)
            {
                return (treePath.LastNode as LineNode) != null;
            }

            public void AddDocument(Document document)
            {
                DocumentNode node = new DocumentNode(document);
                node.HighlightGroup = new HighlightGroup(node.Document.TextEditor);

                int index = this._nodes.BinarySearch(node, new DocumentNodeComparer());

                if (index < 0)
                {
                    index = ~index;
                    this._nodes.Insert(index, node);
                }
                else
                {
                    this._nodes.Add(node);
                }

                this.OnNodesInserted(new TreeModelEventArgs(TreePath.Empty, new int[] { index }, new object[] { node }));
            }

            public void RemoveDocument(Document document)
            {
                List<DocumentNode> keptNodes = new List<DocumentNode>();
                List<object> removedNodes = new List<object>();
                foreach (DocumentNode node in this._nodes)
                {
                    if (node.Document == document) { removedNodes.Add(node); }
                    else { keptNodes.Add(node); }
                }

                this._nodes = keptNodes;
                this.OnNodesRemoved(new TreeModelEventArgs(TreePath.Empty, removedNodes.ToArray())); 
            }

            public void ChangeHighlightColor(Color color)
            {
                this._color = color;
                foreach (DocumentNode node in this._nodes)
                {
                    node.HighlightGroup.ChangeColor(color);
                }
            }

            public void ClearHighlights()
            {
                foreach (DocumentNode node in this._nodes)
                {
                    node.HighlightGroup.ClearMarkers();
                }
            }
        }

        #endregion

        public void AddHighlightButton(Bitmap image, Color color)
        {
            ToolStripButton button = new System.Windows.Forms.ToolStripButton();
            button.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
            button.Image = image;
            button.Size = new System.Drawing.Size(23, 22);
            button.Text = color.ToString();
            button.Tag = color;
            button.Click += new System.EventHandler(this.toolStripButtonHighlight_Click);

            this._highlightButtons.Add(button);
            this.toolStripHighlightColor.Items.Add(button);
        }

        public Color GetHighlightColor()
        {
            foreach (ToolStripButton button in this._highlightButtons)
            {
                if (button.Checked) return (Color) button.Tag;
            }

            return Color.LightCoral;
        }

        public FindResults(string text, bool matchCase, bool matchWholeWordOnly)
        {
            InitializeComponent();

            UnrealDebuggerIDE.Instance.OnDocumentOpened += new UnrealDebuggerIDE.DocumentOpenedDelegate(OnDocumentOpened);
            UnrealDebuggerIDE.Instance.OnDocumentClosed += new UnrealDebuggerIDE.DocumentClosedDelegate(OnDocumentClosed);

            this.Text = text;

            AddHighlightButton(global::UnrealDebugger.Properties.Resources.Type11, Color.LightCoral);
            AddHighlightButton(global::UnrealDebugger.Properties.Resources.Type13, Color.LightGreen);
            AddHighlightButton(global::UnrealDebugger.Properties.Resources.Type14, Color.SkyBlue);
            AddHighlightButton(global::UnrealDebugger.Properties.Resources.Type12, Color.SandyBrown);
            AddHighlightButton(global::UnrealDebugger.Properties.Resources.Type8, Color.Khaki);
            AddHighlightButton(global::UnrealDebugger.Properties.Resources.Type1, Color.Pink);
            AddHighlightButton(global::UnrealDebugger.Properties.Resources.Type2, Color.Orchid);

            ToolStripButton button = this._highlightButtons[_searchCount % this._highlightButtons.Count];
            button.Checked = true;

            this.Icon = System.Drawing.Icon.FromHandle(((Bitmap)button.Image).GetHicon());

            this._text = text;
            this._matchCase = matchCase;
            this._matchWholeWordOnly = matchWholeWordOnly;
            this.Load += new EventHandler(OnLoad);
        }

        void OnDocumentOpened(Document document)
        {
            this._model.AddDocument(document);
        }

        void OnDocumentClosed(Document document)
        {
            this._model.RemoveDocument(document);
        }

        void OnLoad(object sender, EventArgs e)
        {
            this._model = new SearchTreeModel(this._text, this._matchCase, this._matchWholeWordOnly, GetHighlightColor());
            _searchCount++;

            this.treeViewFindResults.BeginUpdate();
            this.treeViewFindResults.Model = this._model;
            this.treeViewFindResults.EndUpdate();
            
            if (this.treeViewFindResults.Root.Children.Count > 0)
            {
                foreach (TreeNodeAdv node in this.treeViewFindResults.Root.Children)
                {
                    DocumentNode docNode = node.Tag as DocumentNode;
                    if (docNode != null && docNode.Document == UnrealDebuggerIDE.Instance.GetDocumentWithFocus())
                    {
                        node.Expand(true);
                        if (node.Children.Count > 0) 
                        {
                            LineNode lineNode = node.Children[0].Tag as LineNode;
                            this.treeViewFindResults.SelectedNode = node.Children[0];
                            this.treeViewFindResults.ScrollTo(this.treeViewFindResults.SelectedNode);

                            if (lineNode != null) { lineNode.GotoLine(); }
                            FocusResults();
                        }
                        break;
                    }
                }

                FocusResults();
            }
        }

        internal void ExpandAll()
        {
            this.treeViewFindResults.ExpandAll();
        }

        internal void GotoLine()
        {
            foreach (TreeNodeAdv node in this.treeViewFindResults.SelectedNodes)
            {
                LineNode lineNode = node.Tag as LineNode;

                if (lineNode != null)
                {
                    lineNode.GotoLine();
                }
            }
        }

        internal void FocusResults()
        {
            this.treeViewFindResults.Focus();
        }

        private void treeViewFindResults_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Return)
            {
                GotoLine();
            }
        }

        private void treeViewFindResults_DoubleClick(object sender, EventArgs e)
        {
            GotoLine();
        }

        private void treeViewFindResults_SelectionChanged(object sender, EventArgs e)
        {
            GotoLine();
            FocusResults();
        }

        private void toolStripButtonHighlight_Click(object sender, EventArgs e)
        {
            ToolStripButton pressedButton = sender as ToolStripButton;

            if (pressedButton != null)
            {
                this.Icon = System.Drawing.Icon.FromHandle(((Bitmap)pressedButton.Image).GetHicon());
                this._model.ChangeHighlightColor((Color) pressedButton.Tag);
                this.OnHighlightColorChanged((Color)pressedButton.Tag);
            }

            foreach (ToolStripItem item in this.toolStripHighlightColor.Items)
            {
                ToolStripButton button = item as ToolStripButton;
                if (button != null)
                {
                    button.Checked = (item == pressedButton);
                }
            }
        }

        private void FindResults_FormClosed(object sender, FormClosedEventArgs e)
        {
            this._model.ClearHighlights();
        }
    }

    #region TextEditorSearcher

    /// <summary>This class finds occurrances of a search string in a text 
    /// editor's IDocument... it's like Find box without a GUI.</summary>
    public class TextEditorSearcher : IDisposable
    {
        IDocument _document;
        public IDocument Document
        {
            get { return _document; }
            set
            {
                if (_document != value)
                {
                    ClearScanRegion();
                    _document = value;
                }
            }
        }

        // I would have used the TextAnchor class to represent the beginning and 
        // end of the region to scan while automatically adjusting to changes in 
        // the document--but for some reason it is sealed and its constructor is 
        // internal. Instead I use a TextMarker, which is perhaps even better as 
        // it gives me the opportunity to highlight the region. Note that all the 
        // markers and coloring information is associated with the text document, 
        // not the editor control, so TextEditorSearcher doesn't need a reference 
        // to the TextEditorControl. After adding the marker to the document, we
        // must remember to remove it when it is no longer needed.
        TextMarker _region = null;
        /// <summary>Sets the region to search. The region is updated 
        /// automatically as the document changes.</summary>
        public void SetScanRegion(ISelection sel)
        {
            SetScanRegion(sel.Offset, sel.Length);
        }
        /// <summary>Sets the region to search. The region is updated 
        /// automatically as the document changes.</summary>
        public void SetScanRegion(int offset, int length)
        {
            var bkgColor = _document.HighlightingStrategy.GetColorFor("Default").BackgroundColor;
            _region = new TextMarker(offset, length, TextMarkerType.SolidBlock,
                bkgColor.HalfMix(Color.FromArgb(160, 160, 160)));
            _document.MarkerStrategy.AddMarker(_region);
        }
        public bool HasScanRegion
        {
            get { return _region != null; }
        }
        public void ClearScanRegion()
        {
            if (_region != null)
            {
                _document.MarkerStrategy.RemoveMarker(_region);
                _region = null;
            }
        }
        public void Dispose() { ClearScanRegion(); GC.SuppressFinalize(this); }
        ~TextEditorSearcher() { Dispose(); }

        /// <summary>Begins the start offset for searching</summary>
        public int BeginOffset
        {
            get
            {
                if (_region != null)
                    return _region.Offset;
                else
                    return 0;
            }
        }
        /// <summary>Begins the end offset for searching</summary>
        public int EndOffset
        {
            get
            {
                if (_region != null)
                    return _region.EndOffset;
                else
                    return _document.TextLength;
            }
        }

        public bool MatchCase;

        public bool MatchWholeWordOnly;

        string _lookFor;
        string _lookFor2; // uppercase in case-insensitive mode
        public string LookFor
        {
            get { return _lookFor; }
            set { _lookFor = value; }
        }

        /// <summary>Finds next instance of LookFor, according to the search rules 
        /// (MatchCase, MatchWholeWordOnly).</summary>
        /// <param name="beginAtOffset">Offset in Document at which to begin the search</param>
        /// <remarks>If there is a match at beginAtOffset precisely, it will be returned.</remarks>
        /// <returns>Region of document that matches the search string</returns>
        public TextRange FindNext(int beginAtOffset, bool searchBackward, out bool loopedAround)
        {
            Debug.Assert(!string.IsNullOrEmpty(_lookFor));
            loopedAround = false;

            int startAt = BeginOffset, endAt = EndOffset;
            int curOffs = beginAtOffset.InRange(startAt, endAt);

            _lookFor2 = MatchCase ? _lookFor : _lookFor.ToUpperInvariant();

            TextRange result;
            if (searchBackward)
            {
                result = FindNextIn(startAt, curOffs, true);
                if (result == null)
                {
                    loopedAround = true;
                    result = FindNextIn(curOffs, endAt, true);
                }
            }
            else
            {
                result = FindNextIn(curOffs, endAt, false);
                if (result == null)
                {
                    loopedAround = true;
                    result = FindNextIn(startAt, curOffs, false);
                }
            }
            return result;
        }

        private TextRange FindNextIn(int offset1, int offset2, bool searchBackward)
        {
            Debug.Assert(offset2 >= offset1);
            offset2 -= _lookFor.Length;

            // Make behavior decisions before starting search loop
            Func<char, char, bool> matchFirstCh;
            Func<int, bool> matchWord;
            if (MatchCase)
                matchFirstCh = (lookFor, c) => (lookFor == c);
            else
                matchFirstCh = (lookFor, c) => (lookFor == Char.ToUpperInvariant(c));
            if (MatchWholeWordOnly)
                matchWord = IsWholeWordMatch;
            else
                matchWord = IsPartWordMatch;

            // Search
            char lookForCh = _lookFor2[0];
            if (searchBackward)
            {
                for (int offset = offset2; offset >= offset1; offset--)
                {
                    if (matchFirstCh(lookForCh, _document.GetCharAt(offset))
                        && matchWord(offset))
                        return new TextRange(_document, offset, _lookFor.Length);
                }
            }
            else
            {
                for (int offset = offset1; offset <= offset2; offset++)
                {
                    if (matchFirstCh(lookForCh, _document.GetCharAt(offset))
                        && matchWord(offset))
                        return new TextRange(_document, offset, _lookFor.Length);
                }
            }
            return null;
        }
        private bool IsWholeWordMatch(int offset)
        {
            if (IsWordBoundary(offset) && IsWordBoundary(offset + _lookFor.Length))
                return IsPartWordMatch(offset);
            else
                return false;
        }
        private bool IsWordBoundary(int offset)
        {
            return offset <= 0 || offset >= _document.TextLength ||
                !IsAlphaNumeric(offset - 1) || !IsAlphaNumeric(offset);
        }
        private bool IsAlphaNumeric(int offset)
        {
            char c = _document.GetCharAt(offset);
            return Char.IsLetterOrDigit(c) || c == '_';
        }
        private bool IsPartWordMatch(int offset)
        {
            string substr = _document.GetText(offset, _lookFor.Length);
            if (!MatchCase)
                substr = substr.ToUpperInvariant();
            return substr == _lookFor2;
        }
    }

    #endregion

    #region TextRange

    public class TextRange : AbstractSegment
    {
        IDocument _document;
        public TextRange(IDocument document, int offset, int length)
        {
            _document = document;
            this.offset = offset;
            this.length = length;
        }
    }

    #endregion

    #region HighlightsGroup

    /// <summary>Bundles a group of markers together so that they can be cleared 
    /// together.</summary>
    public class HighlightGroup : IDisposable
    {
        List<TextMarker> _markers = new List<TextMarker>();
        TextEditorControl _editor;
        IDocument _document;
        public HighlightGroup(TextEditorControl editor)
        {
            _editor = editor;
            _document = editor.Document;
        }
        public void AddMarker(TextMarker marker)
        {
            _markers.Add(marker);
            _document.MarkerStrategy.AddMarker(marker);
        }
        public void ClearMarkers()
        {
            foreach (TextMarker m in _markers)
                _document.MarkerStrategy.RemoveMarker(m);
            _markers.Clear();
            _editor.Refresh();
        }
        public void ChangeColor(Color color)
        {
            List<TextMarker> markers = new List<TextMarker>();
            foreach (TextMarker m in _markers)
            {
                TextMarker n = new TextMarker(m.Offset, m.Length, m.TextMarkerType, color);
                markers.Add(n);
                _document.MarkerStrategy.RemoveMarker(m);
                _document.MarkerStrategy.AddMarker(n);
            }
            _markers = markers;
            _editor.Refresh();
        }
        public void Dispose() { ClearMarkers(); GC.SuppressFinalize(this); }
        ~HighlightGroup() { Dispose(); }

        public IList<TextMarker> Markers { get { return _markers.AsReadOnly(); } }
    }

    #endregion

    #region Globals

	public static class Globals
	{
		public static int InRange(this int x, int lo, int hi)
		{
			Debug.Assert(lo <= hi);
			return x < lo ? lo : (x > hi ? hi : x);
		}
		public static bool IsInRange(this int x, int lo, int hi)
		{
			return x >= lo && x <= hi;
		}
		public static Color HalfMix(this Color one, Color two)
		{
			return Color.FromArgb(
				(one.A + two.A) >> 1,
				(one.R + two.R) >> 1,
				(one.G + two.G) >> 1,
				(one.B + two.B) >> 1);
		}
	}

    #endregion
}
